今天我們要來講解 Flutter 的基礎Widget。
Flutter 預設提供了兩種熱門的 UI 組件,分別是接近 Andorid 原生風格的 Material
以及接近 iOS 原生風格的 Cupertino
可供開發者使用,使得在開發者可以透過呼叫這些定義好的 UI 組件,快速的組合出應用程式。
所以讓我們再重新的檢視一下昨天我們最後寫的 Hello World
程式碼
// 引入 flutter 預載好的 material.dart UI 庫
import 'package:flutter/material.dart';
// 應用程式起始點
void main() {
// 開始執行應用程式,並呼叫 MaterialApp 建構子,表示我的應用程式為 Material style
runApp(MaterialApp(
title: 'Flutter Tutorial',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Playground'),
),
body: Center(
child: Text('Hello World'),
),
),
));
}
我們使用了 Material 作為我們預設的樣式,接著就是不斷的找尋是否有適合的 widget 可以讓我們進行呼叫,開始組合與微調。
由於篇幅因素,內容並無法涵蓋所有的 widget,以下我們將介紹幾個基礎的 widget 使用方法,除了持續壯大我們的 Flutter playground,更藉此能一步步的熟悉 flutter widget 使用方式,最終你就可以自己試著對照文件將各式各樣的 widget 加入到你的應用程式中。
顧名思義 Text
是用於顯示文字的工具,也可以針對文字進行樣式調整、對齊位置等等。我們需要使用到一個 widget 類別時,首先一定得先使用建構子來初始化我們將要宣告的物件。讓我們來看看 Text
的建構子是如何進行定義的
VS Code 檢視 widget class 的使用方式有兩種:
- 將滑鼠的游標移至該 widget 上方,VS Code 便會跳出一個小視窗,裡面會寫定義
- 在 widget 上方點選右鍵 並選擇「移至定義」就會導向定義該 widget 的檔案中
在我的 Flutter 版本中對於Text
建構子如下,不同版本的可能會有些許出入不過並不影響我們怎麼閱讀此元件。在Text
類別的定義中,第一個參數無需具名但是一定要夾帶;剩下用{}
括起來的便是前面章節中學到的named parameters
皆可選擇性的夾帶。
const Text(
String this.data, {
super.key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
this.textHeightBehavior,
this.selectionColor,
}) : ... 後面省略
因此當我們在宣告最基本款的 Text widget
時,我們的宣告方式如下
Text('Hello World');
如此便成功的宣告了一個帶有 Hello World
訊息的 Text widget
。那如果要針對字體進行微調呢?我們來看看上面的 named parameters
中哪個最像是用來定義字體樣式的,應該很好猜就是 style
這個欄位。如果你是從寫前端過來的,當我想將文字樣式改為藍色
且大小為 18px
時,你可能會覺得 style 會是這樣設定:
Text('Hello World', style: 'font-size: 18px; color: blue');
注意!我們在看類別定義時千萬可別漏看了對應的型態!讓我們再一次的前往檢視 style
的型態 (方式與剛剛介紹的相同)。
final TextStyle? style;
首先因為 style
是屬於 optional parameters,因此 TextStyle?
表示賦予的值可為 TextStyle
型態或 null
,表示我們今天要用到 style
時就要找一個型態為 TextStyle
的東西來塞。這時我們再繼續往 TextStyle
的定義往下找:
const TextStyle({
this.inherit = true,
this.color,
this.backgroundColor,
this.fontSize,
this.fontWeight,
this.fontStyle,
this.letterSpacing,
this.wordSpacing,
this.textBaseline,
this.height,
this.leadingDistribution,
this.locale,
this.foreground,
this.background,
this.shadows,
this.fontFeatures,
this.fontVariations,
this.decoration,
this.decorationColor,
this.decorationStyle,
this.decorationThickness,
this.debugLabel,
String? fontFamily,
List<String>? fontFamilyFallback,
String? package,
this.overflow,
}) : ...後面省略
我們來鎖定我們所需要的屬性(字體大小改為 18px;顏色改為藍色)。上方最符合的就是 fontSize
與 color
,兩者分別的型態為 double
與 Color
。因此我們就填入相應所需要型態的值。最終結果如下:
Text('Hello World', style: TextStyle(fontSize: 18, color: Colors.blue));
花了一點篇幅帶大家從 Text
的定義開始像洋蔥一樣一層一層的往裡面剝開,找到我們所需要的屬性再依據所需要的型態給相應的值。其實其他的 widget
也都大同小異,只要掌握到此方法,便可以融會貫通到其他的地方拉!
是由 Material
所定義的按鈕,因此欲引用此 widget
記得要先引入 material package
。我們現在希望可以建立一個 Button ,其中的文字顯示方才的 Hello World 字樣。因此請先將剛剛的文字註解掉,並輸入 ElevatedButton
此時你的 VS Code 應該會跳出自動補全的提示,請按下 Enter
,這時該行應該會變成。
ElevatedButton(onPressed: onPressed, child: child)
我們一樣按照慣例別急著寫,先來檢視 ElevatedButton
的建構子,看看我們需要提供哪些資訊。
const ElevatedButton({
super.key,
required super.onPressed,
super.onLongPress,
super.onHover,
super.onFocusChange,
super.style,
super.focusNode,
super.autofocus = false,
super.clipBehavior = Clip.none,
super.statesController,
required super.child,
});
在類別建構子中全部都是 named parameters
,但因為 onPressed
與 child
兩個參數有標上 required
,也就表示建構此物件時一定要夾帶此兩個參數。
我們就字面上解析這兩個參數的意義:
child
:型態為 Widget?
表示這個 button 底下要放 widget,會被包在此按鈕當中onPressed
:型態為 VoidCallback?
,再往下翻你會發現 typedef VoidCallback = void Function()
,也就是代表 VoidCallback
本身其實是 Function 的另一種寫法。因此 onPressed
是一個函式,用於表示點擊該按鈕後要觸發的事件函式。
typedef
:是用來自定義型別的關鍵字,如:typedef IntList = List<int>
便是定義IntList
這個關鍵字可以用於表示List<int>
這個型態
因此我們就將剛剛的 Text
放入 ElevatedButton
的 child
欄位中。並且希望每次按下按鈕時,都在終端機印出 Hello World
的字樣。
import 'package:flutter/material.dart';
// 應用程式起始點
void main() {
runApp(MaterialApp(
title: 'Flutter Tutorial',
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter Playground'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
debugPrint('Hello World');
},
child: const Text('Hello World',
style: TextStyle(fontSize: 18, color: Colors.white)))),
),
));
}
最後你的結果應該要長這樣,功能可以正常執行,並且編輯器沒有跳出任何的錯誤或是警告。
相信你剛剛在修改的過程當中一定遇到很多次終端機跟你提示「請使用 const 建構子來增進效能」的字樣,原因是因為之前我們有介紹到 const
創建的內容是不變的,表示使用 const
修飾的 widget 在應用程式的生命週期內只會建構一次,使得性能可以有相當大程度的優化。
因此當你的某個部件確定是不會變動的,請使用 const
來進行修飾。不過當你無法判斷時,相信你的編輯器提示會非常「好心」的跳出提示來告訴你,直到你改對為止XD
目前為止,我們介紹了兩種 widget 都僅限於呼叫 widget 並顯示於頁面上。但如果我想要將這些內容進行佈局呢? 這時候就需要一系列的佈局工具 (Layout widget)拉~
其實一開始的程式碼,在 ElevatedButton
之外有使用到 Center
我們還沒有提到。
在 Flutter 中有一個詞為 constraint
(約束),指的是在渲染佈局中的限制或是規則,用於確定 widget 的大小和位置。這樣的約束關係是經由 parent 與 child 經由溝通確認後,再由 parent 傳遞給 child 的,告訴子組件應該要如何進行佈局。一旦子組件超越了 constraint
就會跳出錯誤,表示父組件無法容納。
Center 這個 widget 會根據 constraint 將 child 放置於正中間的位置,如我們的範例一樣。
Padding 是可以用於為子組件添加空白區域,用於調整與周圍的間距。嘗試將方才的 Center widget 拿掉,我們換上 Padding 來看看
Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
onPressed: () {
debugPrint('Hello World');
},
child: const Text('Hello World',
style: TextStyle(fontSize: 18, color: Colors.white))))
我們將 Center 拿掉後,ElevatedButton
會回到預設到左上方 (佈局的預設是由左至右、由上而下),我們設定 child 的 button 四周皆間隔 16 px 的距離。
將多個子組件(children)以水平方向進行排列,不過要注意的是 Row本身並不具備 scroll
的性質,因此在使用前需要先思考是否 children 的寬度足以被 Row 所容納,否則會產生錯誤。請替換成以下程式碼,看看會發生什麼事情
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(children: [
Container(color: Colors.red, width: 100, height: 50),
Container(color: Colors.indigo, width: 100, height: 60),
Container(color: Colors.green, width: 100, height: 70),
]))
原則上你會看到如下圖的運行結果。
我們宣告了三個等寬但高度不同的三個 Container
以水平方向進行排列。現在 Row :
- 寬度:螢幕寬度減去左右的16px padding
- 高度:找出最大高度的 child
這時我們來看看 Row 底下的除了 children 外,兩個也很重要用於對齊的參數:
mainAxisAlignment
:表示水平方向的對齊屬性,預設為 start
表示靠左對齊。其他還有如 end
靠右對齊、center
水平置中對齊等等的屬性crossAxisAlignment
:表示垂直方向的對齊屬性,預設為 center
垂直置中。其他還有如 start
垂直靠上、end
垂直靠下等等的屬性各位可以試著玩玩看,相信你很快就能了解所有屬性!這裡出個小小練習,會在文末進行解答喔~
請修改上述程式碼,使得顯示效果可以如下圖。
與 Row 相對的,Colummn 是將多個子組件(children)以垂直方向進行排列,同樣的是 Column本身並不具備 scroll
的性質,因此在使用前需要先思考是否 children 的長度足以被 Column 所容納,否則會產生錯誤。請替換成以下程式碼,看看會發生什麼事情
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(children: [
Container(color: Colors.red, width: 50, height: 100),
Container(color: Colors.indigo, width: 60, height: 100),
Container(color: Colors.green, width: 70, height: 100),
]))
原則上你會看到如下圖的運行結果。
我們宣告了三個等高但寬度不同的三個 Container
以垂直方向進行排列。現在 Column :
- 高度:螢幕高度減去上下的16px padding
- 寬度:找出最大寬度的 child
同樣我們也來看看 Column 底下的除了 children 外,兩個也用於對齊的參數:
- mainAxisAlignment
:表示垂直方向的對齊屬性,預設為 start
表示靠上對齊。其他還有如 end
靠下對齊、center
垂直置中對齊等等的屬性
- crossAxisAlignment
:表示水平方向的對齊屬性,預設為 center
水平置中。其他還有如 start
水平靠左、end
水平靠右等等的屬性
在 Row 的時候 mainAxisAlignment
中文直翻就是在主要軸的對齊屬性,也就是水平軸;crossAxisAlignment
就是轉 90 度交叉的垂直軸的對齊屬性。
換到 Column 的時候就完全相反,mainAxisAlignment
是垂直軸的對齊屬性;crossAxisAlignment
就是相對的水平軸對齊屬性。
這裡我們也一樣出個練習題,作為本篇的結尾。
請撰寫程式碼,使得顯示效果可以如下圖。這題會同時混用到 Row 與 Column,可以從 Column 方向開始思考。
今天我們認識:
Text
用於基礎顯示文字ElevatedButton
用於顯示按鈕,並認識運用 onPressed
來觸發事件Center
將子組件完全的置中Padding
設定子組件的間隔Row
將多個子組件以水平方向排列;Column
則是將多個子組件以垂直方向排列俗話說「給你魚吃,不如教你釣魚」。我們並無法全盤的介紹一輪所有 widget 的使用方式,但藉由今天的介紹相信大家已經具備了查找文件的能力拉(釣魚🐟)!只要找到好的 widget,看一下文件就能夠引用在你的應用程式中了。
另外Flutter 也有提供 Material 與 Cupertino 各自 component 的樣式任君挑選,趕快來試試找到心儀的 widget 並實作看看吧!
明天我們會來講解 Flutter widget 的重要概念,Stateless
與 Stateful
widget,準備好了嗎!!明天見囉~
Row(
// 水平讓各自 Container 間有間距
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// 垂直靠下
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(color: Colors.red, width: 100, height: 50),
Container(color: Colors.indigo, width: 100, height: 60),
Container(color: Colors.green, width: 100, height: 70),
]))
Column(
// 先考慮 column 佈局
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 第一個 Container 水平靠左
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(color: Colors.red, width: 50, height: 40),
],
),
// 第二個 Container 水平置中
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(color: Colors.indigo, width: 60, height: 40),
],
),
// 第三個 Container 水平靠右
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(color: Colors.green, width: 70, height: 40),
],
),
])